/*
This file is part of IRCaBot.
IRCaBot is IRC logger with features.
Source code: https://notabug.org/acetone/ircabot.
Copyright (C) acetone, 2023.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "irclogger.h"
#include "config.h"
#include "fsconstants.h"

#include <QDir>
#include <QDebug>
#include <QDirIterator>
#include <QFileInfo>

constexpr const int CACHE_SIZE = 64;

IRCLogger::IRCLogger(const QString &network, const QString &channel) :
    m_network(network),
    m_channel(channel),
    m_rootDir(Config::workingDirectory()+QString(FS_NETWORKS_DIR) +FS_NATIVE_SEPARATOR+ QString(network).remove(*FS_NETWORK_NAME_FORMAT_DEL_RGX).toLower()
              +FS_NATIVE_SEPARATOR+ QString(FS_CHATLOG_SUBDIR) +FS_NATIVE_SEPARATOR+ QString(channel).remove('#'))
{
    if (not QFile::exists(m_rootDir))
    {
        if (not QDir().mkpath(m_rootDir))
        {
            qCritical() << "Can't create chat log directory:" << m_rootDir;
        }
    }
    else
    {
        doDateIndex();
    }
}

QList<IRCLogger::Year> IRCLogger::dateIndex() const
{
    QMutexLocker lock (&m_dateIndexMtx);
    return m_dateIndex;
}

void IRCLogger::addMessage(const QString &sender, const QString &message)
{
    QMutexLocker lock (&m_fileIOmtx);

    const auto currentDate = currentDateTime();

    Message msg(sender, message);
    m_cache.add(msg);

    const QString dir = dateDir(currentDate);
    if (not QFile::exists(dir))
    {
        if (not QDir().mkpath(dir))
        {
            qCritical() << "IRCLogger::addMessage can't create directory:" << dir;
            return;
        }
    }

    QFile file (datePath(currentDate));
    bool reindexNeeded = not file.exists();
    if (not file.open(QIODevice::ReadWrite | QIODevice::Append | QIODevice::Text))
    {
        qCritical() << "IRCLogger::addMessage can't save message to file:" << m_network << m_channel << "|" << sender << message;
        return;
    }

    QString dataToWrite;
    if (msg.sender().isEmpty()) // blinded message
    {
        dataToWrite = '.';
    }
    else
    {
        dataToWrite = sender + ": " + message;
    }
    file.write(dataToWrite.toUtf8() + "\n");
    file.close();

    if (reindexNeeded) // TODO: reindex only new data
    {
        doDateIndex();
    }
}

QList<QSharedPointer<IRCLogger::Message>> IRCLogger::getMessages(const QDateTime &date, const quint64 *beginId, const quint64 *endId)
{
    QList<QSharedPointer<IRCLogger::Message>> result;

    const QString filePath = datePath(date);
    if (not QFile::exists(filePath))
    {
        qInfo() << "IRCLogger::getMessages file not exists:" << date << "->" << filePath;
        return result;
    }

    QFile file(filePath);
    if (not file.open(QIODevice::ReadOnly))
    {
        qCritical() << "IRCLogger::getMessages can't read file:" << date << "->" << filePath;
        return result;
    }

    quint64 idCounter = 0;
    QString buffer;
    do
    {
        if (endId != nullptr)
        {
            if (idCounter > *endId) break;
        }

        buffer = file.readLine();
        if (buffer.isEmpty())
        {
            qDebug() << "IRCLogger::getMessages anomaly: empty line in" << filePath;
            continue;
        }

        idCounter++;
        if (beginId != nullptr)
        {
            if (idCounter < *beginId)
            {
                continue;
            }
        }

        QSharedPointer<IRCLogger::Message> msg (new IRCLogger::Message);
        msg->initFromRaw(buffer, idCounter);
        result.push_back(msg);
    }
    while (not file.atEnd());

    file.close();

    return result;
}

void IRCLogger::doDateIndex()
{
    QMutexLocker lock (&m_dateIndexMtx);

    qDebug().noquote() << "["+m_network+"] ["+m_channel+"]" << "Indexing log dates in" << m_rootDir;

    m_dateIndex.clear();

    quint64 yearsCounter = 0;
    quint64 monthCounter = 0;
    quint64 daysCounter = 0;

    QDirIterator yearIterator(m_rootDir);
    while (yearIterator.hasNext())
    {
        auto year = QFileInfo(yearIterator.next());
        if (year.fileName() == "." or year.fileName() == ".." or not year.isDir()) continue;
        bool converted = false;
        int16_t yearNumber = year.fileName().toShort(&converted);
        if (not converted) continue;

        m_dateIndex.push_front({yearNumber, {}});
        yearsCounter++;
    }
    std::sort(m_dateIndex.begin(), m_dateIndex.end());

    for (auto indexIterator = m_dateIndex.begin(); indexIterator != m_dateIndex.end(); indexIterator++)
    {
        QDirIterator monthIterator(m_rootDir + "/" + QString::number(indexIterator->first));
        while (monthIterator.hasNext())
        {
            auto month = QFileInfo(monthIterator.next());
            if (month.fileName() == "." or month.fileName() == ".." or not month.isDir()) continue;
            bool converted = false;
            int8_t monthNumber = month.fileName().toShort(&converted);
            if (not converted) continue;

            indexIterator->second.push_back({monthNumber, {}});
            monthCounter++;
        }
        std::sort(indexIterator->second.begin(), indexIterator->second.end());
    }

    for (auto indexIterator = m_dateIndex.begin(); indexIterator != m_dateIndex.end(); indexIterator++)
    {
        for (auto monthIterator = indexIterator->second.begin(); monthIterator != indexIterator->second.end(); monthIterator++)
        {
            QString monthFolder = QString::number(monthIterator->first);
            if (monthFolder.size() < 2) monthFolder.push_front('0');

            QDirIterator dayIterator(m_rootDir + "/" + QString::number(indexIterator->first) + "/" + monthFolder);
            while (dayIterator.hasNext())
            {
                auto day = QFileInfo(dayIterator.next());
                if (not day.fileName().endsWith(".txt") or not day.isFile()) continue;
                QString fileName = day.fileName();
                static const QRegularExpression rgxOnlyNumbers("[^0-9]");
                fileName.remove(rgxOnlyNumbers);
                bool converted = false;
                int8_t dayNumber = fileName.toShort(&converted);
                if (not converted) continue;

                monthIterator->second.push_back(dayNumber);
                daysCounter++;
            }
            std::sort(monthIterator->second.begin(), monthIterator->second.end());
        }
    }

    qInfo().noquote() << "["+m_network+"] ["+m_channel+"]" << "Log contains" << yearsCounter << "years," << monthCounter << "months and" << daysCounter << "days";
}

QString IRCLogger::dateDir(const QDateTime &date)
{
    return m_rootDir + "/" + date.toString("yyyy/MM");
}

QString IRCLogger::datePath(const QDateTime &date)
{
    return dateDir(date) + "/" + date.toString("dd") + ".txt";
}

IRCLogger::Message::Message(const QString &sender, const QString &text)
{
    if (sender.isEmpty())
    {
        return;
    }
    m_sender = sender;
    m_text = text;
}

void IRCLogger::Message::initFromRaw(const QString &raw, quint64 number)
{
    m_number = number;

    const int delimiterPos = raw.indexOf(':');
    if (delimiterPos < 0 or delimiterPos+1 > raw.size())
    {
        return;
    }

    m_sender = QString(raw).remove(delimiterPos, raw.size() - delimiterPos);
    m_text = QString(raw).remove(0, delimiterPos+1).trimmed();

    const int bridgeDelimiter = m_sender.indexOf('~');
    if (bridgeDelimiter >= 0 and bridgeDelimiter+1 < m_sender.size())
    {
        m_bridge = m_sender;
        m_bridge.remove(bridgeDelimiter, m_bridge.size() - bridgeDelimiter);
        m_sender.remove(0, bridgeDelimiter+1);
    }
}

IRCLogger::Cache::CacheId::CacheId(const QString &raw)
{
    int spacePos = raw.indexOf('_');
    if (spacePos < 0 or spacePos+1 <= raw.size())
    {
        return;
    }

    m_date = QDateTime::fromString(raw.mid(spacePos, raw.size()-spacePos), CACHE_ID_DATE_FORMAT);

    m_number = raw.midRef(spacePos+1).toULongLong();
}

IRCLogger::Cache::CacheId IRCLogger::Cache::back() const
{
    QMutexLocker lock (&m_mtx);

    if (not m_data.isEmpty())
    {
        return m_data.back()->id();
    }
    return CacheId();
}

QList<QSharedPointer<IRCLogger::Message>> IRCLogger::Cache::messagesAfter(const CacheId &id)
{
    QMutexLocker lock (&m_mtx);

    QList<QSharedPointer<IRCLogger::Message>> result;

    if (back() <= id)
    {
        return result;
    }

    foreach(auto msg, m_data)
    {
        if (msg->id() > id)
        {
            result.push_back(msg);
        }
    }

    return result;
}

void IRCLogger::Cache::add(const Message &msg)
{
    QMutexLocker lock (&m_mtx);

    m_data.push_back(QSharedPointer<CachedMessage>(new CachedMessage(msg)));

    while (m_data.size() > CACHE_SIZE)
    {
        m_data.pop_front();
    }
}
